#!/usr/bin/python2.7

# Copyright (c) 2011 CDN-Tool authors. All rights reserved.
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file.

import random
import sys
import json
from httplib import HTTPConnection
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from SocketServer import ThreadingMixIn
from optparse import OptionParser
import time
from urllib2 import urlopen
import urllib
import urlparse
import socket

from cdn_lib import *

class CDNRequestHandler(LoggingMixIn, BaseHTTPRequestHandler):

  logger = LoggingMixIn.log_init("CDNRequestHandler")

  def _acquire(self, blocking=False):
    if self.super_request:
      self._acquired = self.server.reserved_sem.acquire(blocking)
    else:
      self._acquired = self.server.regular_sem.acquire(blocking)

  def _release(self):
    if self._acquired:
      if self.super_request:
        self.server.reserved_sem.release()
      else:
        self.server.regular_sem.release()
    else:
      self.log_error("Tried to release semaphore without acquiring it!")

  def do_HEAD(self):
    """Handles ONLY requests from the hub"""
    path = self.path.split("?")[0]
    if path == "/cdn-hub/remove":
      remote_host = self.path.split("?")[1]
      self.log_info("Deleting %s from the list", remote_host)
      del self.server.files[remote_host]
      self.log_debug("Files: %s", self.server.files)
      response = 200  # Success
    elif path == "/cdn-hub/add":
      try:
        content = self.rfile.read(int(self.headers['Content-Length']))
        update_dict = json.loads(content)
        self.log_info("Updating the list.")
        self.server.files.update(update_dict)
        self.log_debug("Files: %s", self.server.files)
        response = 200  # Success
      except Exception:
        self.log_exception("Wrong add request from the hub!")
        response = 422  # Unprocessable Entity
    else:
      # TODO(bochkarev): redirect to the back-end server
      response = 405  # Method not allowed
    self.send_response(response)
    self.end_headers()

  def do_GET(self):
    """Handles an incoming GET request to the server"""
    self.super_request = self.headers.has_key("Super")
    self.log_debug("self.path = %s", self.path)
    try:
      path, query = self.path.split("?", 1)
    except:
      path, query = self.path, ""
    query_parts = urlparse.parse_qs(query)
    if query_parts.has_key('cdn-server'):
      remote_host = query_parts["cdn-server"][0]
      del query_parts["cdn-server"]
    else:
      remote_host = self.server.address
    if self.server.address != remote_host:
      self._acquire(blocking=True)
      #TODO(bochkarev): think how not to overload neighbour servers.
    else:
      self._acquire()

    self.log_info("File %s at %s requested.", self.path, remote_host)
    if remote_host not in self.server.files:
      self.log_warning("Unknown remote host %s", remote_host)
      self.send_error(404)
      return
    # For now let's redirect client to another server if and only if he's
    # requesting our native file (i.e. remote_host == server address).
    # Also we shouldn't redirect the request if it's sent by another server
    # (i.e. the request starts with ".").
    # TODO(bochkarev): add smart analytics here
    # to avoid excess redirections.
    self.log_info("Path = %s Remote host = %s", path, remote_host)
    list_of_servers = self.server.files.keys()
    list_of_servers.remove(self.server.address)
    if not self._acquired:
      # We need to redirect client to another server.
      query_parts['cdn-server'] = remote_host
      modified_query = urllib.urlencode(query_parts, True)
      # Is this thread-safe?
      redir_address = "http://" + random.choice(list_of_servers) + \
                      path + "?" + modified_query
      self.log_info("Redirecting to %s", redir_address)
      self.send_response(307) # Temporary Redirect
      self.send_header("Location", redir_address)
      self.end_headers()
    else:
      # We need to handle the request and return the file because either
      # remote_host != server address or our file is requested by another server
      original_query = urllib.urlencode(query_parts, True)
      if original_query != '':
        original_path = path + '?' + original_query
      else:
        original_path = path

      while True:
        with self.server.file_lock:
          has_the_file = original_path in self.server.files[remote_host] and \
                         self.server.files[remote_host][original_path]
          has_to_fetch = original_path not in self.server.files[remote_host] or\
                         self.server.files[remote_host][original_path] == None
          if has_to_fetch:
            self.server.files[remote_host][original_path] = False
            break
          if has_the_file:
            break
          time.sleep(random.random()) # Sleep for a random float sec. in [0,1)

      if has_to_fetch:
        # We have to fetch the file from another server.
        if self.server.address == remote_host:
          # It's our native file, we have to fetch it from local webserver.
          try:
            http_connection = HTTPConnection(self.server.webserver)
            self.log_info("Connected to webserver %s", self.server.webserver)
            # TODO(volkhin) handle webserver error
            # TODO(volkhin) save original headers(stored in variable response)
            http_connection.request("GET", original_path)
            self.log_info("Requsted file %s", original_path)
            response = http_connection.getresponse()
            with self.server.file_lock:
              self.server.files[remote_host][original_path] = response.read()
            http_connection.close()
          except Exception:
            self.log_warning("Native file not found!")
            with self.server.file_lock:
              self.server.files[remote_host][original_path] = None
            self.send_error(404)
            # TODO(bochkarev): think about more appropriate error handling here.
        else:
          self.log_info("File not found. Trying to fetch it from the source.")
          addr = remote_host.split(":")
          headers = {"Super": "1"}
          connection = HTTPConnection(addr[0], int(addr[1]) or 80)
          connection.request("GET", self.path, headers=headers)
          response = connection.getresponse()
          self.log_info("Connected to %s. "
                        "File %s requested.", remote_host, original_path)
          if not response.status == 200:
            self.log_info("Error occurred while trying to fetch the file "
                          "from native server")
            # TODO(bochkarev): think about more appropriate error handling here.
            # TODO(bochkarev): handle redirections.
            return
          self.log_info("Got file %s from the native server.", original_path)
          new_file = response.read()
          with self.server.file_lock:
            # Saving the file into our cache.
            self.server.files[remote_host][original_path] = new_file
      # Now we do have the file requested
      time.sleep(self.server.sleep_time) # Imitate file transfer lag
      self.send_response(200) # Success
      self.end_headers()
      self.wfile.write(self.server.files[remote_host][original_path])
      self._release()


class CDNServer(ThreadingMixIn, LoggingMixIn, HTTPServer):

  files = None
  address = None
  sleep_time = None
  webserver = None
  logger = LoggingMixIn.log_init("CDNServer")

  # For handling requests from regular clients.
  regular_sem = None
  # For handling super requests when regular_sem is full.
  reserved_sem = None
  file_lock = None

  def __init__(self, port, webserver="127.0.0.1", max_regular_connections=20,
               max_reserved_connections=10, sleep_time=0):
    self.webserver = webserver
    HTTPServer.__init__(self, (socket.gethostname(), port), CDNRequestHandler)
    self.sleep_time = sleep_time
    self.regular_sem = threading.BoundedSemaphore(max_regular_connections)
    self.reserved_sem = threading.BoundedSemaphore(max_reserved_connections)
    self.file_lock = threading.RLock()
    self.address = (self.socket.getsockname()[0] + ":" +
                     str(self.socket.getsockname()[1]))
    self.log_info("Server bound to %s.", self.address)
    self.files = { self.address: {"/1": None} }
    connection = HTTPConnection(HUB_ADDRESS)
    connection.request("GET", "/add?" + self.address)
    response = connection.getresponse()
    if not response.status == 200:
      raise RuntimeError("Bad response status "
                         "from the hub: %s", response.status)
    self.files[self.address]["/1"] = "BEGIN_" + str(port)*100 + "_END"
    resp = response.read()
    self.log_debug("Hub response: {0}".format(resp))
    update_dict = json.loads(resp)
    if not isinstance(update_dict, dict):
      self.shutdown()
      raise RuntimeError("Response from the hub is not a dict: '%s'.", resp)
    self.files.update(update_dict)
    self.log_info("Successfully loaded servers list.")
    self.log_debug("Files: %s", self.files)

  def shutdown(self):
    connection = HTTPConnection(HUB_ADDRESS)
    connection.request("GET", "/remove?" + self.address)
    connection.getresponse()
    HTTPServer.shutdown(self)

  def __del__(self):
    self.shutdown()


def main():
  parser = OptionParser()
  parser.add_option("-s", "--server", action="store_true",
                                      dest="server",
                                      default=True)
  parser.add_option("-c", "--client", action="store_false", dest="server")
  parser.add_option("--loglevel", dest="loglevel", default="DEBUG")
  parser.add_option("--logfile", dest="logfile")
  parser.add_option("--sleep-time", action="store", type="float",
                    dest="sleep_time", default=0)
  parser.add_option("--max-reg-conn", action="store", type="int",
                    dest="max_reg_conn", default=20)
  parser.add_option("--max-reserved-conn", action="store", type="int",
                    dest="max_reserved_conn", default=10)

  (options, args) = parser.parse_args()
  if len(args) != 2:
    print "Wrong # of args!"
    sys.exit(1)

  set_log_handlers(options.loglevel, options.logfile,
      "CDNServer", "CDNRequestHandler")

  if options.server: # Server side
    server = CDNServer(port=int(args[0]), webserver=args[1],
                       max_regular_connections=options.max_reg_conn,
                       max_reserved_connections=options.max_reserved_conn,
                       sleep_time=options.sleep_time)
    try:
      server.serve_forever()
    except KeyboardInterrupt:
      print "KeyboardInterrupt exception received. Program terminating."
      server.shutdown()
      sys.exit()

  else: # Client side
    server = args[0]
    path = args[1]
    while True:
      path = "/" + path
      reply = urlopen("http://" + server + path)
      print reply.read()
      sys.exit()


if __name__ == "__main__":
  main()
